<template>
	<view class="v-calendar-month-wrapper" ref="v-calendar-month-wrapper">
		<view
			v-for="(item, index) in months"
			:key="index"
			:class="[`v-calendar-month-${index}`]"
			:ref="`v-calendar-month-${index}`"
			:id="`month-${index}`"
		>
			<text v-if="index !== 0" class="v-calendar-month__title"
				>{{ item.year }}年{{ item.month }}月</text
			>
			<view class="v-calendar-month__days">
				<view
					v-if="showMark"
					class="v-calendar-month__days__month-mark-wrapper"
				>
					<text
						class="v-calendar-month__days__month-mark-wrapper__text"
						>{{ item.month }}</text
					>
				</view>
				<view
					class="v-calendar-month__days__day"
					v-for="(item1, index1) in item.date"
					:key="index1"
					:style="[dayStyle(index, index1, item1)]"
					@tap="clickHandler(index, index1, item1)"
					:class="[
						item1.selected &&
							'v-calendar-month__days__day__select--selected',
					]"
				>
					<view
						class="v-calendar-month__days__day__select"
						:style="[daySelectStyle(index, index1, item1)]"
					>
						<text
							class="v-calendar-month__days__day__select__info"
							:class="[
								item1.disabled &&
									'v-calendar-month__days__day__select__info--disabled',
							]"
							:style="[textStyle(item1)]"
							>{{ item1.day }}</text
						>
						<text
							v-if="getBottomInfo(index, index1, item1)"
							class="v-calendar-month__days__day__select__buttom-info"
							:class="[
								item1.disabled &&
									'v-calendar-month__days__day__select__buttom-info--disabled',
							]"
							:style="[textStyle(item1)]"
							>{{ getBottomInfo(index, index1, item1) }}</text
						>
						<text
							v-if="item1.dot"
							class="v-calendar-month__days__day__select__dot"
						></text>
					</view>
				</view>
			</view>
		</view>
	</view>
</template>

<script>
// #ifdef APP-NVUE
// 由于nvue不支持百分比单位，需要查询宽度来计算每个日期的宽度
const dom = uni.requireNativePlugin("dom");
// #endif
import mpMixin from "../../libs/mixin/mpMixin.js";
import mixin from "../../libs/mixin/mixin.js";
import defprops from "../../libs/config/props";
// import dayjs from '../../libs/util/dayjs.js';
import dayjs from "dayjs/esm/index";
export default {
	name: "v-calendar-month",
	mixins: [mpMixin, mixin],
	props: {
		// 是否显示月份背景色
		showMark: {
			type: Boolean,
			default: true,
		},
		// 主题色，对底部按钮和选中日期有效
		color: {
			type: String,
			default: "#3c9cff",
		},
		// 月份数据
		months: {
			type: Array,
			default: () => [],
		},
		// 日期选择类型
		mode: {
			type: String,
			default: "single",
		},
		// 日期行高
		rowHeight: {
			type: [String, Number],
			default: 58,
		},
		// mode=multiple时，最多可选多少个日期
		maxCount: {
			type: [String, Number],
			default: Infinity,
		},
		// mode=range时，第一个日期底部的提示文字
		startText: {
			type: String,
			default: "开始",
		},
		// mode=range时，最后一个日期底部的提示文字
		endText: {
			type: String,
			default: "结束",
		},
		// 默认选中的日期，mode为multiple或range是必须为数组格式
		defaultDate: {
			type: [Array, String, Date],
			default: null,
		},
		// 最小的可选日期
		minDate: {
			type: [String, Number],
			default: 0,
		},
		// 最大可选日期
		maxDate: {
			type: [String, Number],
			default: 0,
		},
		// 如果没有设置maxDate，则往后推多少个月
		maxMonth: {
			type: [String, Number],
			default: 2,
		},
		// 是否为只读状态，只读状态下禁止选择日期
		readonly: {
			type: Boolean,
			default: defprops.calendar.readonly,
		},
		// 日期区间最多可选天数，默认无限制，mode = range时有效
		maxRange: {
			type: [Number, String],
			default: Infinity,
		},
		// 范围选择超过最多可选天数时的提示文案，mode = range时有效
		rangePrompt: {
			type: String,
			default: "",
		},
		// 范围选择超过最多可选天数时，是否展示提示文案，mode = range时有效
		showRangePrompt: {
			type: Boolean,
			default: true,
		},
		// 是否允许日期范围的起止时间为同一天，mode = range时有效
		allowSameDay: {
			type: Boolean,
			default: false,
		},
	},
	data() {
		return {
			// 每个日期的宽度
			width: 0,
			// 当前选中的日期item
			item: {},
			selected: [],
		};
	},
	watch: {
		selectedChange: {
			immediate: true,
			handler(n) {
				this.setDefaultDate();
			},
		},
	},
	computed: {
		// 多个条件的变化，会引起选中日期的变化，这里统一管理监听
		selectedChange() {
			return [this.minDate, this.maxDate, this.defaultDate];
		},
		dayStyle(index1, index2, item) {
			return (index1, index2, item) => {
				const style = {};
				let week = item.week;
				// 不进行四舍五入的形式保留2位小数
				const dayWidth = Number(
					parseFloat(this.width / 7)
						.toFixed(3)
						.slice(0, -1)
				);
				// 得出每个日期的宽度
				// #ifdef APP-NVUE
				style.width = uni.$u.addUnit(dayWidth);
				// #endif
				style.height = uni.$u.addUnit(this.rowHeight);
				if (index2 === 0) {
					// 获取当前为星期几，如果为0，则为星期天，减一为每月第一天时，需要向左偏移的item个数
					week = (week === 0 ? 7 : week) - 1;
					style.marginLeft = uni.$u.addUnit(week * dayWidth);
				}
				if (this.mode === "range") {
					// 之所以需要这么写，是因为DCloud公司的iOS客户端的开发者能力有限导致的bug
					style.paddingLeft = 0;
					style.paddingRight = 0;
					style.paddingBottom = 0;
					style.paddingTop = 0;
				}
				return style;
			};
		},
		daySelectStyle() {
			return (index1, index2, item) => {
				let date = dayjs(item.date).format("YYYY-MM-DD"),
					style = {};
				// 判断date是否在selected数组中，因为月份可能会需要补0，所以使用dateSame判断，而不用数组的includes判断
				if (this.selected.some((item) => this.dateSame(item, date))) {
					style.backgroundColor = this.color;
				}
				if (this.mode === "single") {
					if (date === this.selected[0]) {
						// 因为需要对nvue的兼容，只能这么写，无法缩写，也无法通过类名控制等等
						style.borderTopLeftRadius = "3px";
						style.borderBottomLeftRadius = "3px";
						style.borderTopRightRadius = "3px";
						style.borderBottomRightRadius = "3px";
					}
				} else if (this.mode === "range") {
					if (this.selected.length >= 2) {
						const len = this.selected.length - 1;
						// 第一个日期设置左上角和左下角的圆角
						if (this.dateSame(date, this.selected[0])) {
							style.borderTopLeftRadius = "3px";
							style.borderBottomLeftRadius = "3px";
						}
						// 最后一个日期设置右上角和右下角的圆角
						if (this.dateSame(date, this.selected[len])) {
							style.borderTopRightRadius = "3px";
							style.borderBottomRightRadius = "3px";
						}
						// 处于第一和最后一个之间的日期，背景色设置为浅色，通过将对应颜色进行等分，再取其尾部的颜色值
						if (
							dayjs(date).isAfter(dayjs(this.selected[0])) &&
							dayjs(date).isBefore(dayjs(this.selected[len]))
						) {
							style.backgroundColor = uni.$u.colorGradient(
								this.color,
								"#ffffff",
								100
							)[90];
							// 增加一个透明度，让范围区间的背景色也能看到底部的mark水印字符
							style.opacity = 0.7;
						}
					} else if (this.selected.length === 1) {
						// 之所以需要这么写，是因为DCloud公司的iOS客户端的开发者能力有限导致的bug
						// 进行还原操作，否则在nvue的iOS，uni-app有bug，会导致诡异的表现
						style.borderTopLeftRadius = "3px";
						style.borderBottomLeftRadius = "3px";
					}
				} else {
					if (
						this.selected.some((item) => this.dateSame(item, date))
					) {
						style.borderTopLeftRadius = "3px";
						style.borderBottomLeftRadius = "3px";
						style.borderTopRightRadius = "3px";
						style.borderBottomRightRadius = "3px";
					}
				}
				return style;
			};
		},
		// 某个日期是否被选中
		textStyle() {
			return (item) => {
				const date = dayjs(item.date).format("YYYY-MM-DD"),
					style = {};
				// 选中的日期，提示文字设置白色
				if (this.selected.some((item) => this.dateSame(item, date))) {
					style.color = "#ffffff";
				}
				if (this.mode === "range") {
					const len = this.selected.length - 1;
					// 如果是范围选择模式，第一个和最后一个之间的日期，文字颜色设置为高亮的主题色
					if (
						dayjs(date).isAfter(dayjs(this.selected[0])) &&
						dayjs(date).isBefore(dayjs(this.selected[len]))
					) {
						style.color = this.color;
					}
				}
				return style;
			};
		},
		// 获取底部的提示文字
		getBottomInfo() {
			return (index1, index2, item) => {
				const date = dayjs(item.date).format("YYYY-MM-DD");
				const bottomInfo = item.bottomInfo;
				// 当为日期范围模式时，且选择的日期个数大于0时
				if (this.mode === "range" && this.selected.length > 0) {
					if (this.selected.length === 1) {
						// 选择了一个日期时，如果当前日期为数组中的第一个日期，则显示底部文字为“开始”
						if (this.dateSame(date, this.selected[0]))
							return this.startText;
						else return bottomInfo;
					} else {
						const len = this.selected.length - 1;
						// 如果数组中的日期大于2个时，第一个和最后一个显示为开始和结束日期
						if (
							this.dateSame(date, this.selected[0]) &&
							this.dateSame(date, this.selected[1]) &&
							len === 1
						) {
							// 如果长度为2，且第一个等于第二个日期，则提示语放在同一个item中
							return `${this.startText}/${this.endText}`;
						} else if (this.dateSame(date, this.selected[0])) {
							return this.startText;
						} else if (this.dateSame(date, this.selected[len])) {
							return this.endText;
						} else {
							return bottomInfo;
						}
					}
				} else {
					return bottomInfo;
				}
			};
		},
	},
	mounted() {
		this.init();
	},
	methods: {
		init() {
			// 初始化默认选中
			this.$emit("monthSelected", this.selected);
			this.$nextTick(() => {
				// 这里需要另一个延时，因为获取宽度后，会进行月份数据渲染，只有渲染完成之后，才有真正的高度
				// 因为nvue下，$nextTick并不是100%可靠的
				uni.$u.sleep(10).then(() => {
					this.getWrapperWidth();
					this.getMonthRect();
				});
			});
		},
		// 判断两个日期是否相等
		dateSame(date1, date2) {
			return dayjs(date1).isSame(dayjs(date2));
		},
		// 获取月份数据区域的宽度，因为nvue不支持百分比，所以无法通过css设置每个日期item的宽度
		getWrapperWidth() {
			// #ifdef APP-NVUE
			dom.getComponentRect(
				this.$refs["v-calendar-month-wrapper"],
				(res) => {
					this.width = res.size.width;
				}
			);
			// #endif
			// #ifndef APP-NVUE
			this.$uGetRect(".v-calendar-month-wrapper").then((size) => {
				this.width = size.width;
			});
			// #endif
		},
		getMonthRect() {
			// 获取每个月份数据的尺寸，用于父组件在scroll-view滚动事件中，监听当前滚动到了第几个月份
			const promiseAllArr = this.months.map((item, index) =>
				this.getMonthRectByPromise(`v-calendar-month-${index}`)
			);
			// 一次性返回
			Promise.all(promiseAllArr).then((sizes) => {
				let height = 1;
				const topArr = [];
				for (let i = 0; i < this.months.length; i++) {
					// 添加到months数组中，供scroll-view滚动事件中，判断当前滚动到哪个月份
					topArr[i] = height;
					height += sizes[i].height;
				}
				// 由于微信下，无法通过this.months[i].top的形式(引用类型)去修改父组件的month的top值，所以使用事件形式对外发出
				this.$emit("updateMonthTop", topArr);
			});
		},
		// 获取每个月份区域的尺寸
		getMonthRectByPromise(el) {
			// #ifndef APP-NVUE
			// $uGetRect为vcu-uni-view自带的节点查询简化方法，
			// 组件内部一般用this.$uGetRect，对外的为uni.$u.getRect，二者功能一致，名称不同
			return new Promise((resolve) => {
				this.$uGetRect(`.${el}`).then((size) => {
					resolve(size);
				});
			});
			// #endif

			// #ifdef APP-NVUE
			// nvue下，使用dom模块查询元素高度
			// 返回一个promise，让调用此方法的主体能使用then回调
			return new Promise((resolve) => {
				dom.getComponentRect(this.$refs[el][0], (res) => {
					resolve(res.size);
				});
			});
			// #endif
		},
		// 点击某一个日期
		clickHandler(index1, index2, item) {
			if (this.readonly) {
				return;
			}
			this.item = item;
			const date = dayjs(item.date).format("YYYY-MM-DD");
			if (item.disabled) return;
			// 对上一次选择的日期数组进行深度克隆
			let selected = uni.$u.deepClone(this.selected);
			if (this.mode === "single") {
				// 单选情况下，让数组中的元素为当前点击的日期
				selected = [date];
			} else if (this.mode === "multiple") {
				if (selected.some((item) => this.dateSame(item, date))) {
					// 如果点击的日期已在数组中，则进行移除操作，也就是达到反选的效果
					const itemIndex = selected.findIndex(
						(item) => item === date
					);
					selected.splice(itemIndex, 1);
				} else {
					// 如果点击的日期不在数组中，且已有的长度小于总可选长度时，则添加到数组中去
					if (selected.length < this.maxCount) selected.push(date);
				}
			} else {
				// 选择区间形式
				if (selected.length === 0 || selected.length >= 2) {
					// 如果原来就为0或者大于2的长度，则当前点击的日期，就是开始日期
					selected = [date];
				} else if (selected.length === 1) {
					// 如果已经选择了开始日期
					const existsDate = selected[0];
					// 如果当前选择的日期小于上一次选择的日期，则当前的日期定为开始日期
					if (dayjs(date).isBefore(existsDate)) {
						selected = [date];
					} else if (dayjs(date).isAfter(existsDate)) {
						// 当前日期减去最大可选的日期天数，如果大于起始时间，则进行提示
						if (
							dayjs(
								dayjs(date).subtract(this.maxRange, "day")
							).isAfter(dayjs(selected[0])) &&
							this.showRangePrompt
						) {
							if (this.rangePrompt) {
								uni.$u.toast(this.rangePrompt);
							} else {
								uni.$u.toast(
									`选择天数不能超过 ${this.maxRange} 天`
								);
							}
							return;
						}
						// 如果当前日期大于已有日期，将当前的添加到数组尾部
						selected.push(date);
						const startDate = selected[0];
						const endDate = selected[1];
						const arr = [];
						let i = 0;
						do {
							// 将开始和结束日期之间的日期添加到数组中
							arr.push(
								dayjs(startDate)
									.add(i, "day")
									.format("YYYY-MM-DD")
							);
							i++;
							// 累加的日期小于结束日期时，继续下一次的循环
						} while (
							dayjs(startDate)
								.add(i, "day")
								.isBefore(dayjs(endDate))
						);
						// 为了一次性修改数组，避免computed中多次触发，这里才用arr变量一次性赋值的方式，同时将最后一个日期添加近来
						arr.push(endDate);
						selected = arr;
					} else {
						// 选择区间时，只有一个日期的情况下，且不允许选择起止为同一天的话，不允许选择自己
						if (selected[0] === date && !this.allowSameDay) return;
						selected.push(date);
					}
				}
			}
			this.setSelected(selected);
		},
		// 设置默认日期
		setDefaultDate() {
			if (!this.defaultDate) {
				// 如果没有设置默认日期，则将当天日期设置为默认选中的日期
				const selected = [dayjs().format("YYYY-MM-DD")];
				return this.setSelected(selected, false);
			}
			let defaultDate = [];
			const minDate = this.minDate || dayjs().format("YYYY-MM-DD");
			const maxDate =
				this.maxDate ||
				dayjs(minDate)
					.add(this.maxMonth - 1, "month")
					.format("YYYY-MM-DD");
			if (this.mode === "single") {
				// 单选模式，可以是字符串或数组，Date对象等
				if (!uni.$u.test.array(this.defaultDate)) {
					defaultDate = [
						dayjs(this.defaultDate).format("YYYY-MM-DD"),
					];
				} else {
					defaultDate = [this.defaultDate[0]];
				}
			} else {
				// 如果为非数组，则不执行
				if (!uni.$u.test.array(this.defaultDate)) return;
				defaultDate = this.defaultDate;
			}
			// 过滤用户传递的默认数组，取出只在可允许最大值与最小值之间的元素
			defaultDate = defaultDate.filter((item) => {
				return (
					dayjs(item).isAfter(dayjs(minDate).subtract(1, "day")) &&
					dayjs(item).isBefore(dayjs(maxDate).add(1, "day"))
				);
			});
			this.setSelected(defaultDate, false);
		},
		setSelected(selected, event = true) {
			this.selected = selected;
			event && this.$emit("monthSelected", this.selected, "tap");
		},
	},
};
</script>

<style lang="scss" scoped>
@import "../../libs/css/components.scss";

.v-calendar-month-wrapper {
	margin-top: 4px;
}

.v-calendar-month {
	&__title {
		font-size: 14px;
		line-height: 42px;
		height: 42px;
		color: $v-main-color;
		text-align: center;
		font-weight: bold;
	}

	&__days {
		position: relative;
		@include flex;
		flex-wrap: wrap;

		&__month-mark-wrapper {
			position: absolute;
			top: 0;
			bottom: 0;
			left: 0;
			right: 0;
			@include flex;
			justify-content: center;
			align-items: center;

			&__text {
				font-size: 155px;
				color: rgba(231, 232, 234, 0.83);
			}
		}

		&__day {
			@include flex;
			padding: 2px;
			/* #ifndef APP-NVUE */
			// vue下使用css进行宽度计算，因为某些安卓机会无法进行js获取父元素宽度进行计算得出，会有偏移
			width: calc(100% / 7);
			box-sizing: border-box;
			/* #endif */

			&__select {
				flex: 1;
				@include flex;
				align-items: center;
				justify-content: center;
				position: relative;

				&__dot {
					width: 7px;
					height: 7px;
					border-radius: 100px;
					background-color: $v-error;
					position: absolute;
					top: 12px;
					right: 7px;
				}

				&__buttom-info {
					color: $v-content-color;
					text-align: center;
					position: absolute;
					bottom: 5px;
					font-size: 10px;
					text-align: center;
					left: 0;
					right: 0;

					&--selected {
						color: #ffffff;
					}

					&--disabled {
						color: #cacbcd;
					}
				}

				&__info {
					text-align: center;
					font-size: 16px;

					&--selected {
						color: #ffffff;
					}

					&--disabled {
						color: #cacbcd;
					}
				}

				&--selected {
					background-color: $v-primary;
					@include flex;
					justify-content: center;
					align-items: center;
					flex: 1;
					border-radius: 3px;
				}

				&--range-selected {
					opacity: 0.3;
					border-radius: 0;
				}

				&--range-start-selected {
					border-top-right-radius: 0;
					border-bottom-right-radius: 0;
				}

				&--range-end-selected {
					border-top-left-radius: 0;
					border-bottom-left-radius: 0;
				}
			}
		}
	}
}
</style>
